package dryven.model.di;

import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.LinkedList;
import java.util.List;

import dryven.request.controller.ActionException;

public class DefaultDependencyService extends AbstractDependencyService {

	private Iterable<DependencyInjector> dis;
	
	public DefaultDependencyService(Iterable<DependencyInjector> dis) {
		super();
		this.dis = dis;
	}
	public DefaultDependencyService() {
		super();
	}
	/**
	 * This was taken out of the constructor to avoid a circular dependency
	 * in DependencyInjectingControllerFactory
	 * @param dis
	 */
	public void setInjectors(Iterable<DependencyInjector> dis) {
		this.dis = dis;
	}
	
	public <T> T constructObject(LocalThreadStorage storage, Class<T> type) {
		Constructor<?> bindableConstructorWithMostArguments = null;
		for(Constructor<?> ctor : type.getDeclaredConstructors()) {
			if(bindableConstructorWithMostArguments==null || ctor.getParameterTypes().length>bindableConstructorWithMostArguments.getParameterTypes().length) {
				Class<?>[] paramTypes = ctor.getParameterTypes();
				Annotation[][] paramAnnos = ctor.getParameterAnnotations();
				if(isBindable(paramTypes, paramAnnos,null)) {
					bindableConstructorWithMostArguments = ctor;
				}
			}
		}
		if(bindableConstructorWithMostArguments==null) {
			throw new RuntimeException(String.format("could not bind any constructor for type %s",type.getName()));
		}
		Object[] params = bind(bindableConstructorWithMostArguments.getParameterTypes(),bindableConstructorWithMostArguments.getParameterAnnotations(),null, storage);
		try {
			bindableConstructorWithMostArguments.setAccessible(true);
			T constructedObject = null;
			try {
				constructedObject = (T)bindableConstructorWithMostArguments.newInstance(params);
			} finally {
				bindableConstructorWithMostArguments.setAccessible(false);
			}
			return constructedObject;
		} catch(InvocationTargetException e) {
			throw new ActionException(String.format("Exception while invoking %s constructor",type.getName()),e.getCause());
		}
		catch (Exception e) {
			throw new RuntimeException(e);
		}
	}
	
	public Object invokeMethod(LocalThreadStorage storage, Method m, Object o, Object[] existingParameters) {
		Class<?> firstNotBindableArgType = getFirstNotBindableType(m.getParameterTypes(), m.getParameterAnnotations(), existingParameters);
		if(firstNotBindableArgType!=null) {
			throw new RuntimeException("Could not bind argument "+firstNotBindableArgType.getName()+" for "+m.getDeclaringClass().getName()+"."+m.getName());
		}
		Object[] values = bind(m.getParameterTypes(), m.getParameterAnnotations(), existingParameters, storage);
		try {
			return m.invoke(o, values);
		} catch (IllegalArgumentException e) {
			throw new RuntimeException(e);
		} catch (IllegalAccessException e) {
			throw new RuntimeException(e);
		} catch (InvocationTargetException e) {
			throw new RuntimeException(e);
		}
	}
	
	private boolean isBindable(Class<?>[] paramTypes, Annotation[][] paramAnnos, Object[] existingParameters) {
		return getFirstNotBindableType(paramTypes, paramAnnos, existingParameters)==null;
	}
	
	private Class<?> getFirstNotBindableType(Class<?>[] paramTypes, Annotation[][] paramAnnos, Object[] existingParameters) {
		for(int i=0;i<paramTypes.length;++i) {
			if(
					(existingParameters==null || 
					existingParameters.length>=i || 
					existingParameters[i]==null
			) && getInjectorForParam(paramTypes[i], paramAnnos[i])==null) {
				return paramTypes[i];
			}
		}
		return null;
	}
	
	private Object[] bind(Class<?>[] paramTypes, Annotation[][] paramAnnos, Object[] existingParameters, LocalThreadStorage storage) {
		Object[] values = new Object[paramTypes.length];
		for(int i=0;i<paramTypes.length;++i) {
			if(existingParameters!=null && existingParameters.length>i && existingParameters[i]!=null) {
				values[i] = existingParameters[i];
				continue;
			}
			DependencyInjector di = getInjectorForParam(paramTypes[i], paramAnnos[i]);
			if(di!=null) {
				values[i] = di.load(storage, paramTypes[i], paramAnnos[i]);
			}
		}
		return values;
	}
	
	private DependencyInjector getInjectorForParam(Class<?> type, Annotation[] annos) {
		for (DependencyInjector di : dis) {
			if(di.appliesToType(type, annos)) {
				return di;
			}
		}
		return null;
	}
	@Override
	public Object getDependency(LocalThreadStorage storage, Class<?> type, Annotation[] annotations) {
		return getInjectorForParam(type, annotations).load(storage,type, annotations);
	}
}
